/* 
Copyright 2005-2009, Foundations of Success, Bethesda, Maryland 
(on behalf of the Conservation Measures Partnership, "CMP") and 
Beneficent Technology, Inc. ("Benetech"), Palo Alto, California. 

This file is part of Miradi

Miradi is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 3, 
as published by the Free Software Foundation.

Miradi is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Miradi.  If not, see <http://www.gnu.org/licenses/>. 
*/ 

package org.miradi.diagram.coalescer;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.miradi.diagram.arranger.MeglerArranger;
import org.miradi.exceptions.CommandFailedException;
import org.miradi.exceptions.UnexpectedNonSideEffectException;
import org.miradi.exceptions.UserCanceledException;
import org.miradi.ids.IdList;
import org.miradi.objecthelpers.ORef;
import org.miradi.objecthelpers.ORefList;
import org.miradi.objects.DiagramFactor;
import org.miradi.objects.DiagramLink;
import org.miradi.objects.DiagramObject;
import org.miradi.objects.GroupBox;
import org.miradi.project.Project;
import org.miradi.utils.ProgressInterface;
import org.miradi.views.diagram.LinkCreator;

import com.google.common.collect.Sets;

public class Coalescer
{

	public Coalescer(final DiagramObject diagram, final ProgressInterface progressInterface)
	{
		this.diagram = diagram;
		this.includeObjectTypes = new HashSet<Integer>();
		this.progressInterface = progressInterface;
		NodeManager.reset();
		GroupManager.reset();
	}

	public boolean coalesce(final int... excludeObjectType) throws UnexpectedNonSideEffectException, CommandFailedException, Exception
	{
		if (null == diagram)
			return false;

		buildObjectTypes(excludeObjectType);
		buildNodes();
		buildAllPossibleGroups();
		coalesce();
		makeGroupBoxes();
		createGroupLinks();
		
		return true;
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private void buildObjectTypes(int... excludeObjectType)
	{	
		final Set domain = LinkCreator.getLinkableTypes();
		final Set exclusions = Sets.newHashSet(excludeObjectType);
		this.includeObjectTypes.addAll(domain);
		this.includeObjectTypes.removeAll(exclusions);
	}

	private void buildNodes() throws UserCanceledException
	{
		final Project project = diagram.getProject();
		final IdList linkIds = diagram.getAllDiagramFactorLinkIds();
		final int size = linkIds.size();
		
		progressInterface.setStatusMessage("Building data model...", size);
		for (int i = 0; i < size; i++)
		{
			final DiagramLink link = DiagramLink.find(project, linkIds.getRef(i));
			if (linksValidTypes(link)) {
				makeNodesAndPopulateHelperStructures(link);
			}
			progressInterface.incrementProgress();
		}		
	}
	
	private void buildAllPossibleGroups() throws UserCanceledException
	{
		Set<Node> nodes = NodeManager.getNodes();
		progressInterface.setStatusMessage("Building all possible groups...", nodes.size());
		
		for (Node node : nodes)
		{
			buildGroups(DiagramLink.FROM, node);
			buildGroups(DiagramLink.TO, node);
			progressInterface.incrementProgress();
		}
	}
	
	private void buildGroups(int direction, Node node) 
	{
		Map<Integer,Set<Node>> nodeMap = node.getLinkedNodes(direction);
		for (Integer type : nodeMap.keySet())
		{
			Set<Node> newGroup = nodeMap.get(type);
			GroupManager.maybeNewGroup(direction, newGroup);
		}		
	}
	
	private void coalesce() throws UserCanceledException
	{
		progressInterface.setStatusMessage("Finding best groups...", 1);
		
		boolean notAllNodesGrouped = !NodeManager.allNodesGrouped();
		boolean groupsExist = GroupManager.groupsExist();
		
		while (groupsExist && notAllNodesGrouped) 
		{
			Group largestGroup = GroupManager.dequeueLargestGroup();
			largestGroup.captureNodes();
			notAllNodesGrouped = !NodeManager.allNodesGrouped();
			groupsExist = GroupManager.groupsExist();
		}
		GroupManager.collectGroups();
		
		progressInterface.finished();
	}
	
	private void makeGroupBoxes() throws UnexpectedNonSideEffectException, CommandFailedException, Exception
	{
		Set<Group> groups = GroupManager.getGroups();
		progressInterface.setStatusMessage("Making group boxes...", groups.size());
		for (Group group : groups) {
			MeglerArranger.createAndLinkToGroupBox(diagram, group.getDiagramFactors(), group.getDirection());
			progressInterface.incrementProgress();
		}
	}
		
	private void createGroupLinks() throws Exception
	{
		LinkCreator linkCreator = new LinkCreator(diagram.getProject());
		Set<DiagramFactor> groupBoxDiagramFactors = diagram.getDiagramFactorsThatWrap(GroupBox.getObjectType());
		progressInterface.setStatusMessage("Creating links...", groupBoxDiagramFactors.size());
		for(DiagramFactor groupBoxDiagramFactor : groupBoxDiagramFactors)
		{
			linkCreator.createAllPossibleGroupLinks(diagram, groupBoxDiagramFactor);
			progressInterface.incrementProgress();
		}
	}

	private boolean linksValidTypes(final DiagramLink link)
	{
		final List<Integer> linkTypes = new ArrayList<Integer>();
		final DiagramFactor fromFactor = link.getFromDiagramFactor();
		final DiagramFactor toFactor = link.getToDiagramFactor();
		
		linkTypes.add(fromFactor.getWrappedType());
		linkTypes.add(toFactor.getWrappedType());
		linkTypes.addAll(maybeExtractGroupedTypes(fromFactor,toFactor));
		
		return includeObjectTypes.containsAll(linkTypes);
	}

	private Set<Integer> maybeExtractGroupedTypes(final DiagramFactor... diagramFactors)
	{
		final Set<Integer> groupedTypes = new HashSet<Integer>();
		for (DiagramFactor diagramFactor : diagramFactors) 
		{
			if (GroupBox.is(diagramFactor.getWrappedType())) {
				ORefList groupedRefs = diagramFactor.getGroupBoxChildrenRefs();
				int groupedType = groupedRefs.getFirstElement().getObjectType();
				groupedTypes.add(groupedType);
			}
		}
		return groupedTypes;
	}

	private void makeNodesAndPopulateHelperStructures(final DiagramLink link)
	{
		DiagramFactor fromFactor = link.getFromDiagramFactor();
		DiagramFactor toFactor = link.getToDiagramFactor();
		
		Set<Node> froms = getNodes(fromFactor);
		Set<Node> tos = getNodes(toFactor);
		
		for (Node from : froms)
			from.add(DiagramLink.TO, new ArrayList<Node>(tos).toArray(new Node[tos.size()]));
		
		for (Node to : tos)
			to.add(DiagramLink.FROM, new ArrayList<Node>(froms).toArray(new Node[froms.size()]));
	}

	private Set<Node> getNodes(DiagramFactor diagramFactor)
	{
		final Project project = diagram.getProject();
		Set<Node> nodes = new HashSet<Node>();
		if (GroupBox.is(diagramFactor.getWrappedType())) {
			ORefList groupedRefs = diagramFactor.getGroupBoxChildrenRefs();
			for (int i = 0; i < groupedRefs.size(); i++) {
				ORef oRef = groupedRefs.get(i);
				DiagramFactor groupedDiagramFactor = DiagramFactor.find(project, oRef);
				Node node = NodeManager.get(groupedDiagramFactor);
				nodes.add(node);
			}
		} else {
			Node node = NodeManager.get(diagramFactor);
			nodes.add(node);
		}
		return nodes;
	}

	private final DiagramObject diagram;
	private final Set<Integer> includeObjectTypes;
	private final ProgressInterface progressInterface;
	
}
